<audio id="tick" autobuffer="autobuffer">
  <!-- Reenable when this Chromium bug is fixed:
    http://code.google.com/p/chromium/issues/detail?id=31024
  <source src="tick.ogg" type="audio/ogg; codecs=vorbis">
  -->
  <source src="http://chromodoro.googlecode.com/hg/tick.ogg"
          type="audio/ogg; codecs=vorbis">
</audio>
<audio id="ring" autobuffer="autobuffer">
  <!-- Reenable when this Chromium bug is fixed:
    http://code.google.com/p/chromium/issues/detail?id=31024
  <source src="ring.ogg" type="audio/ogg; codecs=vorbis">
  -->
  <source src="http://chromodoro.googlecode.com/hg/ring.ogg"
          type="audio/ogg; codecs=vorbis">
</audio>
<canvas id="icon" width="19" height="19"></canvas>
<script>
function EventLog(key) {
  this.key = key;
}

EventLog.prototype.logEvent = function(event) {
  if (localStorage["enable_logging"] == "enabled") {
    return;
  }
  event.date = new Date();
  var count = this.getCount();
  var item_key = this.key + ":item:" + count;
  localStorage.setItem(item_key, JSON.stringify(event));
  this.bucketize(event.date, count);
  localStorage[this.key + ":count"] = count + 1;
};

EventLog.prototype.bucketize = function(date, idx) {
  var bucket_key = this.key + ":bucket";
  var components = [date.getFullYear(), date.getMonth(), date.getDate()];
  for (var i = 0; i < components.length; i++) {
    bucket_key += ":" + components[i];
    if (!localStorage[bucket_key]) {
      localStorage[bucket_key] = idx;
    }
  }
};

EventLog.prototype.getCount = function() {
  var count = localStorage[this.key + ":count"];
  if (count == null) {
    count = 0;
  } else {
    count = Number(count);
  }
  return count;
};

EventLog.prototype.getIterator = function() {
  return new EventLogIterator(this, 0, this.getCount());
};

EventLog.prototype.getEvent = function(idx) {
  var item_key = this.key + ":item:" + idx;
  return JSON.parse(localStorage[item_key]);
};


function EventLogIterator(event_log, start, end) {
  this.event_log = event_log;
  this.start = start;
  this.end = end;
}

EventLogIterator.prototype.hasNext = function() {
  return this.start < this.end;
}

EventLogIterator.prototype.next = function() {
  return this.event_log.getEvent(this.start++);
};


function ChromodoroEvent(action, task, max_duration) {
  this.action = action;
  this.task = task;
  this.max_duration = max_duration;
}


var event_log = new EventLog("chromodoro");
event_log.logEvent(new ChromodoroEvent("init", null, null));


// Here is our state model:
//
// IDLING <-----> WORKING
//    ^             /
//     \           /
//      \         /
//       \       V
//        RESTING
//
// During the IDLING state, our timer is set to the work duration and is
// paused.
//
// During the WORKING state, our timer ticks down from the work duration. Once
// the timer expires, we transition automatically to the RESTING state.
//
// During the RESTING state, our timer ticks down from the rest duration. Once
// the timer expires, we transition automatically to the IDLING state.
var IDLING = 0;
var WORKING = 1;
var RESTING = 2;

// Our initial state is IDLING.
var state = IDLING;

// Evil global variables.

// setTimeout handle used by the timer.
var timeout;

// The callback to invoke when the timer expires. Should be one of startResting
// or startIdling.
var timeout_callback;

// The timestamp (ms) when the timer started.
var start_time;

// The future timestamp (ms) when the timer expires.
var end_time;

// Amount of milliseconds remaining until the timer expires.
var remainder_ms;

// Timer progress as a percentage (0 <= progress <= 1).
var progress;

// List of registered observer callbacks, to invoke each timer tick or state
// change.
var observers = [];

// Boolean that observers can flip on to cause a desktop notification to be
// displayed after a tick.
var notificationRequested = false;

// Reference to the tick audio element.
var tick = document.getElementById("tick");

// Reference to the ring audio element.
var ring = document.getElementById("ring");

// Reference to our canvas for drawing the browser action icon.
var icon = document.getElementById("icon");


// A dual-purpose progress bar renderer. Takes a canvas and periodically
// redraws a progress bar in it.
//
// Works in two modes: icon and popup. Icon mode is for a tiny, non-interactive
// version. Popup mode features an emulated "stop" button for interrupting the
// timer and returning to the IDLING state.
function ProgressBar(canvas, icon_mode, opt_interactive) {
  this.interactive = opt_interactive == null || opt_interactive;
  this.canvas = canvas;
  this.icon_mode = icon_mode;
  this.ctx = this.canvas.getContext("2d");
  this.stop_button_offset = this.icon_mode ? 0 : this.canvas.height / 5;
  this.stop_button_size = this.icon_mode ? 0 : this.canvas.height - 2 * this.stop_button_offset;
  this.bgcolor = "white";
  var self = this;
  if (!icon_mode && this.interactive) {
    this.canvas.onclick = function(e) { self.onClick(e); };
  }
}

ProgressBar.prototype.onClick = function(e) {
  if (state == IDLING) {
    return;
  }
  var third = this.canvas.height / 3;
  var low = this.stop_button_offset;
  var high = low + this.stop_button_size;
  if (e.offsetX >= low && e.offsetY >= low
      && e.offsetX <= high && e.offsetY <= high) {
    startIdling(false);
  }
};

ProgressBar.prototype.update = function(percentage, color, label) {
  this.Clear();
  var height = this.icon_mode ? 8 : this.canvas.height;
  var width = percentage * this.canvas.width;
  if (this.interactive &&
      width < 2 * this.stop_button_offset + this.stop_button_size) {
    width = 2 * this.stop_button_offset + this.stop_button_size;
  }
  this.DrawGlossyRectangle(color, 0, 0, width, height);
  if (this.icon_mode) {
    this.DrawIcon();
    this.ctx.strokeStyle = "black";
    this.ctx.strokeRect(0, 0, this.canvas.width, 8);
  } else if (this.interactive) {
    var third = this.canvas.height / 3;
    this.DrawRectangle(interpolateBetweenColors(color, [0, 0, 0, 0xff], 0.3),
                       this.stop_button_offset, this.stop_button_offset,
                       this.stop_button_size, this.stop_button_size);
    // TODO: Make this a pause/play button
    /*
    var light_color = interpolateBetweenColors(color, [0xff, 0xff, 0xff, 0xff], 0.2);
    this.DrawRectangle(light_color,
                       this.stop_button_offset + this.stop_button_size / 5,
                       this.stop_button_offset + this.stop_button_size / 5,
                       this.stop_button_size / 5, 3 * this.stop_button_size / 5);
    this.DrawRectangle(light_color,
                       this.stop_button_offset + 3 * this.stop_button_size / 5,
                       this.stop_button_offset + this.stop_button_size / 5,
                       this.stop_button_size / 5, 3 * this.stop_button_size / 5);
    */
  }
};

ProgressBar.prototype.DrawGlossyRectangle = function(color, x, y, w, h) {
  var white = [0xff, 0xff, 0xff, 0xff];
  var black = [0, 0, 0, 0xff];
  var light_color = interpolateBetweenColors(color, white, 0.3);
  var dark_color = interpolateBetweenColors(color, black, 0.2);

  // Basic rounded-corner rectangle with gradient.
  var corner_r = this.icon_mode ? 0 : 4;
  this.ctx.beginPath();
  this.ctx.moveTo(x + corner_r, y);
  this.ctx.arc(x + w - corner_r, y + corner_r, corner_r, 3 * Math.PI / 2, 2 * Math.PI, false);
  this.ctx.arc(x + w - corner_r, y + h - corner_r, corner_r, 0, Math.PI / 2, false);
  this.ctx.arc(x + corner_r, y + h - corner_r, corner_r, Math.PI / 2, Math.PI, false);
  this.ctx.arc(x + corner_r, y + corner_r, corner_r, Math.PI, 3 * Math.PI / 2, false);
  var gradient = this.ctx.createLinearGradient(x, y, x, y + h);
  gradient.addColorStop(0, colorTupleToCss(color));
  gradient.addColorStop(corner_r / h, colorTupleToCss(light_color));
  gradient.addColorStop(1 - corner_r / h, colorTupleToCss(color));
  gradient.addColorStop(1, colorTupleToCss(dark_color));
  this.ctx.fillStyle = gradient;
  this.ctx.strokeStyle = colorTupleToCss(dark_color);
  this.ctx.fill();

  // Curvy path to draw gloss
  var gloss_h = h / 3;
  corner_r /= 2;
  this.ctx.globalCompositeOperation = "lighter";
  this.ctx.fillStyle = colorTupleToCss([64, 64, 64, 255]);
  this.ctx.beginPath();
  this.ctx.moveTo(x, y);
  this.ctx.lineTo(x + w, y);
  this.ctx.arc(x + w - corner_r, y + gloss_h - corner_r, corner_r, 0, Math.PI / 2, false);
  this.ctx.arc(x + corner_r, y + gloss_h + corner_r, corner_r, 3 * Math.PI / 2, Math.PI, true);
  this.ctx.lineTo(x, y);
  this.ctx.fill();
  this.ctx.globalCompositeOperation = "source-over";
};

ProgressBar.prototype.Clear = function() {
  this.ctx.fillStyle = this.bgcolor;
  this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
};

ProgressBar.prototype.DrawRectangle = function(color, x, y, w, h) {
  this.ctx.fillStyle = colorTupleToCss(color);
  this.ctx.fillRect(x, y, w, h);
};

ProgressBar.prototype.DrawIcon = function() {
  this.ctx.beginPath();
  this.ctx.moveTo(6, 2);
  this.ctx.lineTo(14, 2);
  this.ctx.lineTo(10, 6);
  this.ctx.closePath();
  this.ctx.fillStyle = "black";
  this.ctx.fill();
}


// Returns remainder_ms formatted as [m]m:ss.
function formatTimeRemaining() {
  var s = Math.floor(remainder_ms / 1000);
  var h = Math.floor(s / 60);
  var m = s % 60;
  if (m < 10) {
    m = "0" + m;
  }
  return h + ":" + m;
}

function interpolateBetweenColors(color1, color2, perc) {
  // Interpolate.
  var r1 = color1[0];
  var g1 = color1[1];
  var b1 = color1[2];
  var a1 = color1[3];
  var r2 = color2[0];
  var g2 = color2[1];
  var b2 = color2[2];
  var a2 = color2[3];
  var r = Math.floor((r2 - r1) * perc + r1);
  var g = Math.floor((g2 - g1) * perc + g1);
  var b = Math.floor((b2 - b1) * perc + b1);
  var a = Math.floor((a2 - a1) * perc + a1);
  return [r, g, b, a];
}

function colorTupleToCss(color) {
  return "rgba(" + color[0] + ", " + color[1] + ", " + color[2] + ", " + color[3] + ")";
}

// Determines the "mood" of the progress bar according to the state and how
// much time is left.
function getProgressBackgroundColor() {
  var left, right;
  switch (state) {
    case IDLING:
      // White.
      return [0, 0, 0, 255];
    case WORKING:
      // Transition from green to red.
      left = [0x66, 0xff, 0x66, 0xff];
      right = [0xff, 0x00, 0x00, 0xff];
      break;
    case RESTING:
      // Transition from light to dark blue.
      left = [0xaa, 0xaa, 0xff, 0xff];
      right = [0x33, 0x33, 0xff, 0xff];
      break;
  }
  return interpolateBetweenColors(left, right, progress);
}

// Retrieves the title of the last task that was started.
function getLastTaskTitle() {
  return localStorage["last_task_title"];
}

// Transitions to the WORKING state and records the task title.
function startWorking(task_title) {
  if (state != this.IDLING) {
    // TODO(logan): Figure out error handling.
    return;
  }
  localStorage["last_task_title"] = task_title;
  state = WORKING;
  var duration = localStorage["work_duration_mins"] * 60 * 1000;
  event_log.logEvent(new ChromodoroEvent("work", task_title, duration));
  startTimer(duration, startResting);
};

// Transitions to the RESTING state.
function startResting() {
  if (state != WORKING) {
    // TODO(logan): Figure out error handling.
    return;
  }
  state = RESTING;
  var duration = localStorage["rest_duration_mins"] * 60 * 1000;
  event_log.logEvent(new ChromodoroEvent("rest", localStorage["last_task_title"], duration));
  showNotice();
  startTimer(duration, startIdling);
};

// Transitions to the IDLING state.
function startIdling(opt_notify) {
  var notify = opt_notify == null || opt_notify;
  if (timeout != null) {
    clearTimeout(timeout);
    timeout = null;
  }
  event_log.logEvent(new ChromodoroEvent("rest", null, null));
  remainder_ms = localStorage["work_duration_mins"] * 60 * 1000;
  progress = 0;
  if (notify && state != IDLING) {
    showNotice();
  }
  state = IDLING;

  // Make sure tick audio will be ready for when the user starts a new task.
  // Until we use a local resource, we must use load to "rewind."
  tick.load();

  updateProgress();
}

function showNotice() {
  var notice = webkitNotifications.createHTMLNotification("notification.html");
  notice.show();
}

// Determines what delay we should pass to setTimeout. We want our callback to
// be invoked as soon after the timer tick as possible.
function predictNextTick(now) {
  return 1000 - (now - start_time) % 1000;
}

// Initializes the timer and starts it running using a setTimeout loop.
function startTimer(duration_ms, callback) {
  timeout_callback = callback;
  start_time = (new Date()).getTime();
  end_time = start_time + duration_ms;
  progress = 0;

  // If starting work, play the tick sound.
  if (state == WORKING && tick.readyState >= tick.HAVE_CURRENT_DATA
      && !tick.seeking) {
    if (localStorage["enable_audio"] != "disabled") {
      tick.play();
    }
  }

  timeout = setTimeout(updateTimer, 0);
};

// Register callbacks for timer ticks.
function registerObserver(observer) {
  observers.push(observer);
}

// Callback for timer ticks.
function updateTimer() {
  // Notify observers, update the UI, and get the current time.
  var now = updateProgress();
  // Determine if the timer has expired.
  if (now >= end_time) {
    // Play the ring sound to notify the user that time is up.
    if (ring.readyState >= ring.HAVE_CURRENT_DATA && !ring.seeking) {
      if (localStorage["enable_audio"] != "disabled") {
        ring.play();
        // Make sure ring audio will be ready for when the timer expires.
        // Until we use a local resource, we must use load to "rewind."
        setTimeout(function() { ring.load(); }, 3000);
      }
    }
    timeout_callback();
  } else {
    // Schedule the next tick.
    timeout = setTimeout(updateTimer, predictNextTick(now));
  }
};

// Notifies all observers of the current timer status and updates the UI.
// This is called after every timer tick, as well as on every state change.
// Returns the current timestamp in ms.
function updateProgress() {
  var now = (new Date()).getTime();
  if (now >= end_time) {
    now = end_time;
  }
  if (state != IDLING) {
    // Update progress and remaining time according to timer.
    progress = (now - start_time) / (end_time - start_time);
    remainder_ms = end_time - now;
  }

  var remainder_msg = formatTimeRemaining();
  var bgcolor = getProgressBackgroundColor();

  // Update the icon and its badge.
  updateIcon(progress, bgcolor, remainder_msg);
  chrome.browserAction.setBadgeBackgroundColor({"color": bgcolor});
  chrome.browserAction.setBadgeText({"text": remainder_msg});

  // Update popup.html if it's loaded.
  var newObservers = [];
  notificationRequested = false;
  for (var i = 0; i < observers.length; i++) {
    if (observers[i](progress, bgcolor, remainder_msg)) {
      newObservers.push(observers[i]);
    }
  }
  observers = newObservers;
  if (notificationRequested) {
    showNotice();
  }

  return now;
}

// Updates the browser action icon's progress bar.
function updateIcon(progress, bgcolor, remainder_msg) {
  icon_progress_bar.update(progress, bgcolor, remainder_msg);
  var ctx = icon.getContext("2d");
  chrome.browserAction.setIcon({"imageData": ctx.getImageData(0, 0, this.icon.width, this.icon.height)});
}

// Stores a default value for an option in localStorage if it hasn't been set
// there already.
function setDefault(name, value) {
  if (localStorage[name] == null) {
    localStorage[name] = value;
  }
}

// Options and their default values:
setDefault("work_duration_mins", 25);
setDefault("rest_duration_mins", 5);

// Instantiate the browser action icon progress bar and transition into our
// initial IDLING state.
var icon_progress_bar = new ProgressBar(icon, true);
startIdling();
</script>
